//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

namespace Microsoft.Research.ScientificWorkflow.UIDesigner
{
    #region using directives
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
    using System.Reflection;
    using System.ComponentModel;
    using Microsoft.Research.ScientificWorkflow.TridentModel;
    using Microsoft.Research.ScientificWorkflow.TridentUtilities;
    using Microsoft.Research.ScientificWorkflow.TridentComposer;
    using System.Xml;
    using Microsoft.Practices.EnterpriseLibrary.Logging;
    using System.Resources;
    using System.Workflow.ComponentModel.Compiler;
    using Microsoft.Research.ScientificWorkflow.Validators;
    using TridentAPI;
    using Er = Microsoft.Research.DataLayer;
    #endregion

    /// <summary>
    /// Interaction logic for ParamCtrl.xaml.
    /// </summary>
    public partial class ParameterControl : UserControl
    {
        /// <summary>
        /// Holds the fill color of Shape.
        /// </summary>
        private Brush paramShapeFillBrush;

        /// <summary>
        /// Holds the Stroke color of Shape.
        /// </summary>
        private Brush paramShapeStrokeBrush;

        /// <summary>
        /// The fill color of the ellipse.
        /// </summary>
        private Brush ellipseFillColor;

        /// <summary>
        /// The stroke color of the triangular border.
        /// </summary>
        private SolidColorBrush triangleBorderColor;

        /// <summary>
        /// Specifies if this parameter is for input or output.
        /// </summary>
        private ParameterFlowType parameterType;

        /// <summary>
        /// The parameter description model.
        /// </summary>
        private TridentUIActivityElement parent;

        /// <summary>
        /// Parameter Description Model.
        /// </summary>
        private ParameterDescriptionModel paramModel;

        /// <summary>
        /// The composer control holding this element.
        /// </summary>
        private ComposerControl composerCtrl;

        /// <summary>
        /// Constructor with provision for specifying the type.
        /// </summary>
        /// <param name="paramType">
        /// Specify if the control is to be used as a input parameter or an output parameter.
        /// </param>
        /// <param name="parent">
        /// The parent of the activity.
        /// </param>
        /// <param name="paramModel">
        /// The parameter model representation.
        /// </param>
        public ParameterControl(ParameterFlowType parameterType, TridentUIActivityElement parent, ParameterDescriptionModel parameterModel)
            : base()
        {
            InitializeComponent();
            this.ParameterType = parameterType;
            this.parent = parent;
            this.paramModel = parameterModel;
            this.Loaded += new RoutedEventHandler(ParameterControl_Loaded);
            this.Unloaded += new RoutedEventHandler(ParameterControl_Unloaded);
            this.DefaultEllipseFillBrush = this.ellipse.Fill;

            // Unique name for automation.
            if (!string.IsNullOrEmpty(parent.Name))
            {
                // Rectify names.
                string name = parent.Name + "_" + parameterModel.PropertyName;
                name = name.Replace(" ", string.Empty);
                name = name.Replace(".", string.Empty);
                this.Name = name;
            }
        }

        /// <summary>
        /// Gets the default Ellipse fill color.
        /// </summary>
        public Brush DefaultEllipseFillBrush { get; private set; }

        /// <summary>
        /// Gets or sets the property for Fill color.
        /// </summary>
        public Brush ParameterShapeFillBrush
        {
            get
            {
                return this.paramShapeFillBrush;
            }

            set
            {
                if (value != null)
                {
                    this.paramShapeFillBrush = value;
                    this.shapeParam.Fill = this.paramShapeFillBrush;
                    this.paramShapeStrokeBrush = this.paramShapeFillBrush;
                }
            }
        }

        /// <summary>
        /// Gets or sets the property for Stroke color. 
        /// </summary>
        public Brush ParameterShapeStrokeBrush
        {
            get
            {
                return this.paramShapeStrokeBrush;
            }

            set
            {
                if (value != null)
                {
                    this.paramShapeStrokeBrush = value;
                    this.shapeParam.Stroke = this.paramShapeStrokeBrush;
                }
            }
        }

        /// <summary>
        /// Gets or sets the fill color for the ellipse.
        /// </summary>
        public Brush EllipseFillColor
        {
            get
            {
                return this.ellipseFillColor;
            }
            set
            {
                this.ellipseFillColor = value;
                this.ellipse.Fill = this.ellipseFillColor;
            }
        }

        /// <summary>
        /// Gets or sets the parameter type.
        /// </summary>
        public ParameterFlowType ParameterType
        {
            get
            {
                return this.parameterType;
            }

            set
            {
                this.parameterType = value;
            }
        }

        /// <summary>
        /// Gets or sets the color of the triangular border.
        /// </summary>
        public SolidColorBrush TriangleBorderColor
        {
            get
            {
                return this.triangleBorderColor;
            }
            set
            {
                this.triangleBorderColor = value;
                this.triangleBorder.Stroke = value;
            }
        }

        /// <summary>
        /// Loaded event handler for the control.
        /// </summary>
        /// <param name="sender">source</param>
        /// <param name="e">event arguments</param>
        private void ParameterControl_Loaded(object sender, RoutedEventArgs e)
        {
            this.GetComposerControl();
            this.AddInteractionHandlers();
        }

        /// <summary>
        /// UnLoaded event handler for the control.
        /// </summary>
        /// <param name="sender">source</param>
        /// <param name="e">event arguments</param>
        private void ParameterControl_Unloaded(object sender, RoutedEventArgs e)
        {
            this.RemoveInteractionHandlers();
        }

        /// <summary>
        /// Add the interaction handlers according to the mode.
        /// </summary>
        private void AddInteractionHandlers()
        {
            if (this.composerCtrl != null && this.composerCtrl.CurrentRenderMode == RenderMode.ComposerMode)
            {
                this.Drop += new DragEventHandler(this.OnInputParamDrop);
                this.DragOver += new DragEventHandler(this.OnInputParamDragOver);

                if (ParameterFlowType.InputParameter == this.parameterType)
                {
                    this.parentGrd.AllowDrop = true;
                    this.PreviewMouseLeftButtonDown += new MouseButtonEventHandler(this.OnInputParameterDoubleClicked);
                }
                else
                {
                    this.parentGrd.AllowDrop = false;
                    this.PreviewMouseLeftButtonDown += new MouseButtonEventHandler(this.OnOutputParamDragStart);
                }
            }
        }

        /// <summary>
        /// Remove the interaction handlers according to the mode.
        /// </summary>
        private void RemoveInteractionHandlers()
        {
            if (this.composerCtrl != null && this.composerCtrl.CurrentRenderMode == RenderMode.ComposerMode)
            {
                this.Drop -= new DragEventHandler(this.OnInputParamDrop);
                this.DragOver -= new DragEventHandler(this.OnInputParamDragOver);

                if (ParameterFlowType.InputParameter == this.parameterType)
                {
                    this.PreviewMouseLeftButtonDown -= new MouseButtonEventHandler(this.OnInputParameterDoubleClicked);
                }
                else
                {
                    this.PreviewMouseLeftButtonDown -= new MouseButtonEventHandler(this.OnOutputParamDragStart);
                }
            }
        }

        /// <summary>
        /// Fired on mouse left button down, handles the drag start functionality.
        /// </summary>
        /// <param name="sender">
        /// This.
        /// </param>
        /// <param name="e">
        /// MouseButtonEventArgs.
        /// </param>
        private void OnOutputParamDragStart(object sender, MouseButtonEventArgs e)
        {
            if (e.LeftButton == MouseButtonState.Pressed)
            {
                try
                {
                    List<BindingValidator> validateBindings = GetValue(WorkflowActivityElement.ValidateBindingProperty) as List<BindingValidator>;
                    bool validationRequired = (bool)GetValue(WorkflowActivityElement.ValidationRequiredProperty);
                    Dictionary<string, object> draggedDataDiction = ((FrameworkElement)e.OriginalSource).DataContext as Dictionary<string, object>;
                    ParameterDescriptionModel paramModelDragged = draggedDataDiction["ParameterDescriptionModel"] as ParameterDescriptionModel;
                    ParameterDragInfo paramInf = new ParameterDragInfo(
                        paramModelDragged,
                        this.parent.Model,
                        validationRequired,
                        validateBindings);

                    DragDrop.DoDragDrop((System.Windows.DependencyObject)e.OriginalSource, paramInf, DragDropEffects.Copy);
                }
                catch (Exception exception)
                {
                    TridentErrorHandler.HandleUIException(exception);
                    TridentMessageBox.ShowTridentErrorMessageBox(Application.Current.MainWindow, TridentResourceManager.GetString("ParameterElemMappingFailed"));
                }
            }
        }

        /// <summary>
        /// Fired on drop over this triangle.
        /// </summary>
        /// <param name="sender">
        /// This.
        /// </param>
        /// <param name="e">
        /// DragEventArgs.
        /// </param> 
        private void OnInputParamDrop(object sender, DragEventArgs e)
        {
            ParameterDragInfo dragInfo = (ParameterDragInfo)e.Data.GetData(typeof(ParameterDragInfo));
            if (null == dragInfo)
            {
                return;
            }

            if (null != this.parent)
            {
                Visibility currentVisibility = FetchModelCurrentVisibility(this.parent.Model);
                if (Visibility.Visible != currentVisibility)
                {
                    TridentMessageBox.ShowTridentErrorMessageBox(
                        Application.Current.MainWindow,
                        TridentResourceManager.GetString("DestinationActivityHiddenError"));
                    return;
                }
            }

            Visibility sourceModelVisibility = FetchModelCurrentVisibility(dragInfo.SourceModel);
            if (Visibility.Visible != sourceModelVisibility)
            {
                TridentMessageBox.ShowTridentErrorMessageBox(
                    Application.Current.MainWindow,
                    TridentResourceManager.GetString("SourceActivityHiddenError"));
                return;
            }

            if (false == IsIFElseParameterDropValid(dragInfo))
            {
                TridentMessageBox.ShowTridentErrorMessageBox(
                    Application.Current.MainWindow,
                    TridentResourceManager.GetString("ParameterIfBranchMapping"));
                return;
            }

            if (false == IsForParameterDropValid(dragInfo))
            {
                TridentMessageBox.ShowTridentErrorMessageBox(
                    Application.Current.MainWindow,
                    TridentResourceManager.GetString("ParameterElemInvalidDropValidationFailed"));
                return;
            }

            if (this.paramModel.IsMandatoryForWorkflow)
            {
                TridentMessageBox.ShowTridentErrorMessageBox(
                                  Application.Current.MainWindow,
                                  TridentResourceManager.GetString("ParamElementWFInputError"));
                return;
            }

            // Check if the parameter on which is dropped already databound.
            if (this.paramModel.IsDatabound)
            {
                TridentMessageBox.ShowTridentErrorMessageBox(
                    Application.Current.MainWindow,
                    TridentResourceManager.GetString("ParameterElemInputParamPreBound"));
                return;
            }
            else
            {
                if (dragInfo.ValidationRequired)
                {
                    string validateAgainst = GetValue(WorkflowActivityElement.ValidatePropertyNameProperty) as string;
                    if (!ParameterControl.CheckValidDrop(dragInfo.ValidateBindings, validateAgainst))
                    {
                        TridentMessageBox.ShowTridentErrorMessageBox(
                            Application.Current.MainWindow,
                            TridentResourceManager.GetString("ParameterElemInvalidDropValidationFailed"));
                        return;
                    }
                }

                // Create binding.
                this.BindProperty(dragInfo.ActivityName, dragInfo.Model);
                this.composerCtrl.RefreshUI();
                this.composerCtrl.IsWFCommitted = false;
            }
        }

        /// <summary>
        /// Validates whether param drop is between if true, if false
        /// Return false if the drop is with in same IF true else branch
        /// Returns true if the drop ia assross IF Else (ie From IfElse1 to IFElse2)
        /// </summary>
        /// <param name="dragInfo">dragInfo</param>
        /// <returns>True if drop is valid False otherwise</returns>
        private bool IsIFElseParameterDropValid(ParameterDragInfo dragInfo)
        {
            bool validateresult = true;
            string sourceActivityClassName = string.Empty;
            string destinationActivityClassName = string.Empty;
            string sourceParentId = string.Empty;
            string destinationeParentId = string.Empty;
            if (null != dragInfo && null != dragInfo.SourceModel && null != dragInfo.SourceModel.ParentModel
                && null != dragInfo.SourceModel.ParentModel.ParentModel && null != dragInfo.SourceModel.ParentModel.ParentModel.ParentModel)
            {
                sourceActivityClassName = this.GetActivityClassName(dragInfo.SourceModel.ParentModel);
                // Get the Parallel activity unique ID
                sourceParentId = dragInfo.SourceModel.ParentModel.ParentModel.ParentModel.UniqueId;
            }

            if (null != this.parent && null != this.parent.Model && null != this.parent.Model.ParentModel
                && null != parent.Model.ParentModel.ParentModel && null != parent.Model.ParentModel.ParentModel.ParentModel)
            {
                destinationActivityClassName = this.GetActivityClassName(this.parent.Model.ParentModel);
                // Get the Parallel activity unique ID
                destinationeParentId = parent.Model.ParentModel.ParentModel.ParentModel.UniqueId;
            }

            if (sourceActivityClassName.Equals(UIDesignerCodeConstants.ACTIVITYCLASSIFTRUE, StringComparison.OrdinalIgnoreCase)
                && destinationActivityClassName.Equals(UIDesignerCodeConstants.ACTIVITYCLASSIFFALSE, StringComparison.OrdinalIgnoreCase)
                && sourceParentId.Equals(destinationeParentId, StringComparison.OrdinalIgnoreCase))
            {
                validateresult = false;
            }

            if (sourceActivityClassName.Equals(UIDesignerCodeConstants.ACTIVITYCLASSIFFALSE, StringComparison.OrdinalIgnoreCase)
                && destinationActivityClassName.Equals(UIDesignerCodeConstants.ACTIVITYCLASSIFTRUE, StringComparison.OrdinalIgnoreCase)
                && sourceParentId.Equals(destinationeParentId, StringComparison.OrdinalIgnoreCase))
            {
                validateresult = false;
            }

            return validateresult;
        }

        /// <summary>
        /// Validates whether param drop in for activity is valid
        /// </summary>
        /// <param name="dragInfo">dragInfo</param>
        /// <returns>True if drop is valid False otherwise</returns>
        private bool IsForParameterDropValid(ParameterDragInfo dragInfo)
        {
            bool validateresult = true;
            string sourceActivityClassName = string.Empty;
            string destinationActivityClassName = string.Empty;
            string sourceActivityUniqueId = string.Empty;
            string destinationActivityUniqueId = string.Empty;

            // get the Source Activity name & Unique id
            if (null != dragInfo && null != dragInfo.SourceModel)
            {
                sourceActivityClassName = dragInfo.SourceModel.ActivityClass;
                sourceActivityUniqueId = dragInfo.SourceModel.UniqueId;
            }

            // get the Destination Activity name & Unique id
            if (null != this.parent && null != this.parent.Model)
            {
                destinationActivityClassName = this.parent.Model.ActivityClass;
                destinationActivityUniqueId = this.parent.Model.UniqueId;
            }

            // Check if the Drag and drop happen inside For activity
            if (sourceActivityClassName.Equals(UIDesignerCodeConstants.ACTIVITYCLASSFOR, StringComparison.OrdinalIgnoreCase)
                && destinationActivityClassName.Equals(UIDesignerCodeConstants.ACTIVITYCLASSFOR, StringComparison.OrdinalIgnoreCase))
            {
                // Check if the Drag and drop happened for same For activity
                // Ignore if its happen between different for activity
                if (sourceActivityUniqueId.Equals(destinationActivityUniqueId, StringComparison.OrdinalIgnoreCase))
                {
                    validateresult = false;
                }
            }

            return validateresult;
        }
        
        /// <summary>
        /// Get the activity class name if the activity class name is either if true or if false return the same
        /// if not check the parent class name Recursively 
        /// string.emty otherwise
        /// </summary>
        /// <param name="model">ActivityModel</param>
        /// <returns>ActivityClassName</returns>
        private string GetActivityClassName(BaseModel model)
        {
            if (model.IsCompositeActivityModel)
            {
                CompositeActivityModel compositeModel = model as CompositeActivityModel;
                if (compositeModel.ActivityClass.Equals("TridentBasicActivities.Activities.IfTrue", StringComparison.OrdinalIgnoreCase)
                           || compositeModel.ActivityClass.Equals("TridentBasicActivities.Activities.IfFalse", StringComparison.OrdinalIgnoreCase))
                {
                    return compositeModel.ActivityClass;
                }
                else if (compositeModel.ParentModel != null)
                {
                    return GetActivityClassName(compositeModel.ParentModel);
                }
                else
                {
                    return string.Empty;
                }
            }
            else
            {
                return string.Empty;
            }
        }

        /// <summary>
        /// On drag over the input param. In case of the dragged object not being compatible, drop is disallowed.
        /// </summary>
        /// <param name="sender">
        /// Drop area.
        /// </param>
        /// <param name="e">
        /// Drag EventArgs.
        /// </param>
        private void OnInputParamDragOver(object sender, DragEventArgs e)
        {
            ParameterDragInfo dragInfo = (ParameterDragInfo)e.Data.GetData(typeof(ParameterDragInfo));
            if (null == dragInfo)
            {
                e.Effects = DragDropEffects.None;
            }
            else
            {
                e.Effects = DragDropEffects.All;
                if (dragInfo.SourceModel.Equals(this.parent.Model) && dragInfo.Model.Name.Equals(this.paramModel.Name))
                {
                    e.Effects = DragDropEffects.None;
                }
            }
        }

        /// <summary>
        /// Fired on double click of the input parameter.
        /// </summary>
        /// <param name="sender">
        /// This.
        /// </param>
        /// <param name="e">
        /// MouseButtonEventArgs.
        /// </param>
        private void OnInputParameterDoubleClicked(object sender, MouseButtonEventArgs e)
        {
            // Handles the logic of double click.
            if (e.ClickCount != 2)
            {
                return;
            }

            Dictionary<string, object> draggedDataDiction = ((FrameworkElement)e.OriginalSource).DataContext as Dictionary<string, object>;
            ParameterDescriptionModel paramModel = draggedDataDiction["ParameterDescriptionModel"] as ParameterDescriptionModel;

            // Check if the parameter is already bound.
            if (this.paramModel.IsDatabound)
            {
                TridentMessageBox.ShowTridentErrorMessageBox(
                    Application.Current.MainWindow,
                    TridentResourceManager.GetString("ParameterElemInputParamPreBound"));
                return;
            }

            try
            {
                Er.TypeInitializer typeInit = TypeProviderHelper.FetchTypeInitializer(paramModel.DataType, Er.Runtime.WPF);
                if (typeInit != null)
                {
                    string deserializedVal = (paramModel.Value != null) ? paramModel.Value.ToString() : string.Empty;
                    TypeInitializerContainer typeInitContainer = new TypeInitializerContainer();
                    if (typeInitContainer != null)
                    {
                        BaseTypeInitializer baseTypeinit = typeInitContainer.DoModal(typeInit, deserializedVal);
                        if (baseTypeinit != null)
                        {
                            paramModel.Value = baseTypeinit.GetSerializedVersion(baseTypeinit.GetValue());
                            this.ToolTip = ActivityParameterDesigner.CreateParameterTooltip(paramModel);
                            this.composerCtrl.IsWFCommitted = false;
                        }
                    }

                    return;
                }
            }
            catch (TridentCustomException ex)
            {
                TridentErrorHandler.HandleTridentCustomExceptionInUI(ex);
            }

            // Retreive the current value.
            object currentValue = paramModel.Value;

            
            bool allowNullInput = true;
            ////If the Activity input is mandatory and the Workflow input is not, then null input is not valid for the actvity input parameter
            if (paramModel.IsMandatory && !paramModel.IsMandatoryForWorkflow)
            {
                allowNullInput = false;
            }
            // Open the dialog box for the user to input values.
            InputBoxResult result =
                InputBox.Show("Set an Input Parameter", "Enter a new value for the " + paramModel.PropertyName + /* " of " + activityMetadata.Name + */":", currentValue == null ? "" : currentValue.ToString(), allowNullInput);

            if (result.ButtonResult == MessageBoxResult.OK)
            {
                try
                {
                    object parsedValue = null;
                    Type dataType = Type.GetType(paramModel.DataType);

                    // If the type is one of the buitin(in GAC) datatypes then convert it.
                    if (dataType != null)
                    {
                            if (string.IsNullOrEmpty(result.UserInput) && !(paramModel.IsMandatory && !paramModel.IsMandatoryForWorkflow))
                        {
                            parsedValue = null;
                        }
                        else
                        {
                            parsedValue = TypeDescriptor.GetConverter(dataType).ConvertFromString(result.UserInput);
                        }
                    }
                    else
                    {
                        // Else just store the string. Conversion happens during creation of component activity model.
                        parsedValue = result.UserInput;
                    }

                        if (paramModel.IsMandatory && !paramModel.IsMandatoryForWorkflow)
                    {
                        if (parsedValue != null)
                        {
                            paramModel.Value = parsedValue;
                            this.composerCtrl.IsWFCommitted = false;
                        }
                    }
                    else
                    {
                        paramModel.Value = parsedValue;
                        this.composerCtrl.IsWFCommitted = false;
                    }
                }
                catch (NotSupportedException ex)
                {
                    TridentErrorHandler.HandleUIException(ex);
                    TridentMessageBox.ShowTridentErrorMessageBox(
                        Application.Current.MainWindow,
                        TridentResourceManager.GetString("ParameterElemInvalidInput"));
                }
                catch (Exception ex)
                {
                    TridentErrorHandler.HandleUIException(ex);
                    TridentMessageBox.ShowTridentErrorMessageBox(
                        Application.Current.MainWindow,
                        TridentResourceManager.GetString("ParameterElemInvalidInput"));
                }
            }
        }

        /// <summary>
        /// Binds the output of the dragged control to the input of the dropped control.
        /// </summary>
        /// <param name="sourceName">
        /// The source name.
        /// </param>
        /// <param name="source">
        /// The source parameter.
        /// </param>
        /// <param name="targetElem">
        /// Name of the target element.
        /// </param>
        /// <param name="target">
        /// The target element.
        /// </param>
        private void BindProperty(
            string sourceName,
            ParameterDescriptionModel source)
        {
            this.paramModel.CreateBindings(sourceName, source.PropertyName);
        }

        private static bool CheckValidDrop(List<BindingValidator> validateBindings, string validateName)
        {
            bool IsValid = true;
            foreach (BindingValidator bindValidator in validateBindings)
            {
                if (!bindValidator.ValidateBinding(validateName))
                {
                    IsValid = false;
                    break;
                }
            }

            return IsValid;
        }

        /// <summary>
        /// Get the composer control instance.
        /// </summary>
        private void GetComposerControl()
        {
            string composerCtrlName = (string)this.GetValue(ComposerControl.ComposerControlNameProperty);
            this.composerCtrl = Application.Current.MainWindow.FindName(composerCtrlName) as ComposerControl;
        }

        /// <summary>
        /// Gets the visibility of the base model.
        /// </summary>
        /// <param name="model">
        /// BaseModel.
        /// </param>
        /// <returns>
        /// Visibility.
        /// </returns>
        private static Visibility FetchModelCurrentVisibility(BaseModel model)
        {
            Visibility currentVisibility = Visibility.Visible;
            if (model != null)
            {
                currentVisibility = model.CurrentParameterBindingVisibility;
            }

            return currentVisibility;
        }
    }
}
